<?php
/*
 * This file is part of the BooXtore Student project.
 *
 * (c) Francis Backeland f.backeland@gmail.com
 * (c) Laurine Chaplot chaplot.laurine@orange.fr
 * (c) Julien Dutot julien.dutot@gmail.com
 * (c) Romain Guillaume romain-guillaume@orange.fr
 * (c) Guillaume Paringaux g.paringaux@gmail.com
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace BXTR\AdminBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;

use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;

use BXTR\AdminBundle\Form\AdminListForm;
use BXTR\AdminBundle\Form\AdminEditForm;
use BXTR\CoreBundle\Controller\InitializableEntityControllerInterface;

use Symfony\Component\Form\Exception\TransformationFailedException;
use BXTR\DatatablesBundle\Form\DataTransformer\EntityToNumberTransformer;

/**
 * Entity forms Controller
 *
 * Provide generic add/edit, remove and list controllers used to edit database entities.
 * Routes using this controller are generated using the the bxtr_admin.entities parameters.
 *
 */
class EntityController extends Controller implements InitializableEntityControllerInterface
{
    /**
     * Holds the form configuration (Manly fields and translations).
     *
     * @var Array
     */
    protected $params = null;

    /**
     * Hold the name of the entity used to build the params array.
     *
     * @var mixed
     */
    protected $entityName = null;

    /**
     * Doctrine entity manager.
     *
     * @var Doctrine\ORM\EntityManager
     */
    protected $em;

    /**
     * translator
     *
     * @var Symfony\Component\Translation\Translator
     */
    protected $translator;

    /**
     * accessor
     *
     * @var Symfony\Component\PropertyAccess\PropertyAccess
     */
    protected $accessor;

    /**
     * Register static properties.
     *
     */
    public function __construct()
    {
        $this->accessor = PropertyAccess::createPropertyAccessor();
    }

    /**
     *  initialize is fired because this controller implements InitializableControllerInterface.
     *  This method is called soon after the constructor and have service loading capabilities 
     *  ($this->get(<service>) work).
     *
     */
    public function initialize(Request $request)
    {
        $this->em = $this->getDoctrine()->getManager();
        $this->translator = $this->get('translator');
    }
    
    /**
     * Fired by the BXTR\AdminBundle\EventListener\InjectControllerEntityControllerListener event.
     * This call is configured on the route configuration, with the entityName parameter.
     *
     * @param mixed $entityName
     */
    public function selectEntity($entityName) {
        $this->registerEntityParameters($entityName);
    }

    /**
     * Get the current entity parameters, built from bxtr_admin.entities parameters.
     *
     * @return array
     *
     */
    public function params()
    {
        if ($this->params === null) {
           throw new \Exception('registerEntityParameters() should be called before using params') ;
        }
        return $this->params;
    }

    /**
     * Display a list of entity with a DatatablesSelectorType form.
     * From the list we can add a new category, remove or edit the selected
     * entity or go back to the administation index.
     *
     */
    public function listAction(Request $request)
    {
        $params = $this->params();

        // initialize the datatables form using the current entity informations.
        $form = $this->createForm(new AdminListForm($this->em), null, array(
            'back_url' => $this->generateUrl($params['routes']['base']),
            'remove_popup_url' => $this->generateUrl($params['routes']['remove_popup'], array('id' => '__ID__')),
            'class' => $params['data_class'],
            'fields' => $params['fields'],
        ));

        $form->handleRequest($request);

        if ($form->isValid()) {
            // test wich button is clicked and apply redirects
            if ($form->get('add')->isClicked()) {
                return $this->redirect($this->generateUrl($params['routes']['add']));
            }

            if ($form->get('edit')->isClicked()) {
                return $this->redirect($this->generateUrl($params['routes']['edit'], array(
                    'id' => $this->getEntityIdentifier($form->get('list')->getData()),
                )));
            }
        }

        return $this->render($params['views']['list'], Array(
            'form' => $form->createView(),
            'main_title' => $this->translator->trans($params['l18n']['list_title']),
        ));
    }

    
    /**
     * Display a new entity record for edition.
     */
    public function addAction(Request $request, $entityName)
    {
        $params = $this->params();

        // Create a new entity from the table name notation
        $entClass = $this->em
            ->getClassMetadata($params['data_class']);
        $entity = new $entClass->name;

        $title = $this->get('translator')->trans($params['l18n']['add_title']);

        return $this->finishEditForm($request, $entity, $title, true);
    }

    /**
     * Display an edit form
     *
     * @param Request $request
     * @param mixed   $id
     */
    public function editAction(Request $request, $id, $entityName)
    {
        $params = $this->params();

        // fetch entity from id.
        $transformer = new EntityToNumberTransformer($this->getListQuery());

        try {
            $entity = $transformer->reverseTransform($id);
        } catch (TransformationFailedException $e) {
            throw $this->createNotFoundException($this->notFoundMesage());
        }

        $title = $this->translator->trans($params['l18n']['edit_title'], array(
            '%name%' => $this->findEntityName($entity),
        ));

        return $this->finishEditForm($request, $entity, $title); 

    }

    public function removePopupAction(Request $request, $id, $entityName)
    {
        $params = $this->params();
        // fetch entity from id.
        $transformer = new EntityToNumberTransformer($this->getListQuery());

        try {
            $entity = $transformer->reverseTransform($id);
        } catch (TransformationFailedException $e) {
            throw $this->createNotFoundException($this->notFoundMesage());
        }

        // Begin form building
        $builder = $this->createFormBuilder();
        $message = $this->deleteMessage($entity);

        $this->buildRemoveForm($builder, $entity);
        $form = $builder
            ->setAction($this->generateUrl($params['routes']['remove_popup'], array('id' => $id)))
            ->getForm();

        $form->handleRequest($request);

        if ($form->isValid()) {
            if ($form->get('remove')->isClicked()) {
                $this->doRemove($entity, $form);
                $this->em->flush();
            }

            return $this->redirect($this->generateUrl($params['routes']['list']));
        }

        return $this->render('BXTRAdminBundle:Layout:DeletePopup.html.twig', array(
            'form' => $form->createView(),
            'message' => $message,
        ));
    }

    /**
     * Actually build the form for the add and edit action using the entity and title provided.
     *
     * @param Request $request
     * @param mixed $entity
     * @param mixed $title
     * @param bool $addition inform that the call is made from an add or edit action.
     */
    protected function finishEditForm(Request $request, $entity, $title, $addition=false) {

        $params = $this->params();

        $form = $this->createForm(new AdminEditForm($this->em), $entity, array(
            'back_url' => $this->generateUrl($params['routes']['list']),
            'class' => $params['data_class'],
            'data_class' => $params['data_class'],
            'fields' => $params['fields']
        ));

        $form->handleRequest($request);

        if ($form->isValid()) {

            $this->doPersist($entity, $form, $addition);
            $this->em->flush();

            return $this->redirect($this->generateUrl($params['routes']['list']));
        }
        
        return $this->render($params['views']['edit'], array(
            'form' => $form->createView(),
            'main_title' => $title
        ));      
    }

    protected function buildRemoveForm($builder, $entity)
    {
        foreach($this->params()['on_remove']['shift'] as $fieldName) {
            $count = $this->accessor->getValue($entity, $fieldName)->count();
            if ($count > 0) {
                $qb = $this->getListQuery();
                $qb = $qb
                    ->andWhere($qb->expr()->neq($qb->getRootAliases()[0] . '.' . $this->getEntityIdentifierField(), ':id'))
                    ->setParameters(array('id' => $this->getEntityIdentifier($entity)));

                $builder->add('shift_' . $fieldName, 'entity', array(
                    'mapped' => false,
                    'class' => $this->params()['data_class'],
                    'query_builder' =>  $qb,
                    'label' => 'Ent.' . \ucfirst($this->entityName) . '.Fields.' . \ucfirst($fieldName),
                    'required' => $this->params()['on_remove']['cascade'] === array(),
                ));
            }   
        }
        $builder->add('remove', 'remove_button');
    }

    protected function doRemove($entity, $form)
    {
        foreach($this->params()['on_remove']['shift'] as $fieldName) {
            $target = $form->get('shift_' . $fieldName)->getData();
            if ($target !== null) {
                $methodName = $this->em->getClassMetadata($this->params()['data_class'])->associationMappings[$fieldName]['mappedBy'];
                foreach($this->accessor->getValue($entity, $fieldName) as $ent) {
                    $this->accessor->setValue($ent, $methodName, $target);
                }
            }
        }

        foreach($this->params()['on_remove']['setnull'] as $fieldName) {
            $methodName = $this->em->getClassMetadata($this->params()['data_class'])->associationMappings[$fieldName]['mappedBy'];
            foreach($this->accessor->getValue($entity, $fieldName) as $ent) {
                $this->accessor->setValue($ent, $methodName, null);
            }
        }

        foreach($this->params()['on_remove']['cascade'] as $entName) {
            // @TODO check this, it call entity:getEditions on book, could be replaced with a query
            // with the entity parameters.
            // Also if the config name is different from the entity name, it will not work.
            foreach($this->accessor->getValue($entity, $entName) as $ent) {
                $this->em->remove($ent);
            }
        }
        $this->em->remove($entity);
    }

    protected function doPersist($entity, $form)
    {
        $this->em->persist($entity);
    }
    
    protected function findEntityName($entity) {
        if (method_exists($entity, '__toString')) {
            return $entity;
        }
        try {
            return $this->accessor->getValue($entity, 'name'); 
        } catch (NoSuchPropertyException $e) {
            return $this->getEntityIdentifier($entity);
        }
    }

    /**
     * Return the name of the first identifier field of an entity 
     * (the one registered under the id: parameter in the doctrine mapping files).
     *
     */
    protected function getEntityIdentifierField() {
        return $this->em->getClassMetadata($this->params()['data_class'])->identifier[0];
    }

    /**
     * Return the value of the first identifier field of an entity.
     *
     * @param mixed $entity
     */
    protected function getEntityIdentifier($entity)
    {
        return $this->accessor->getValue($entity, $this->getEntityIdentifierField());
    }

    
    /**
     * Return a queryBuilder wich select the entities displayed on the list, it
     * is also used to transform selected ids and to fetch the list used when "shifting" child to 
     * another entity before removing.
     *
     */
    protected function getListQuery()
    {
        return $this->em->createQueryBuilder()
            ->add('select', 'e') 
            ->add('from', $this->params()['data_class'] . ' e'); 
    }

    /**
     * Used as return value for the actions when an entity is not found.
     *
     */
    protected function notFoundMesage()
    {
        return $this->translator->trans('Ent.Default.notFound');
    }

    /**
     * Grab the name and the informations of the entities that will be manupulated on the actions.
     *
     * @param mixed $entityName
     */
    protected function registerEntityParameters($entityName)
    {   if ($this->entityName !== null) {
            throw new \Exception('registerEntityParameters should only be called once');
        }
        $this->entityName = $entityName;
        $this->params = $this->container->getParameter('bxtr_admin.entities')[$entityName];
        $this->resolveLateParameters();
        return $this->params;
    }

    /**
     * Append parameters that need services to be configured
     * like the guessed fields that require doctrine mappings
     *
     * Those parameters should be resolved in a early Request event
     * but since they are currently only used on this controller or 
     * his decendants, the resolving should be called less frequently from here.
     *
     */
    protected function resolveLateParameters() {

        $entity = $this->params;

        $guessType = $this->params['guess_fields']; // none|both|list|edit

        $meta = $this->em->getClassMetadata($this->params['data_class']);

        if ($guessType != 'none') { // if the field should not have been guessed, his visibility will be set to none.
            $fields = $meta->fieldNames;
            $identifier = $meta->identifier[0];

            foreach ($fields as $fieldName) {
                $visibility = ($fieldName != $identifier)? 'both': 'none';

                // If the guess should be only on list or edit, hide the field on the other views.
                if ($guessType === 'edit') {
                    $visibility = 'edit';
                } elseif ($guessType === 'list') {
                    $visibility = 'list';
                }

                // if the field is not in the table, initialize it to null
                $sc =& $this->params['fields'][$fieldName];
                if ($sc === null) {// the row was just created
                    $sc = array(
                         // All definition required fields should be created here
                        'transformer' => null,//$customTransformer,
                        'label' => null,
                        'type' => null,
                        'visibility' => $visibility
                    );
                }
           }
        }
        
        foreach($this->params['fields'] as $fieldName => $params) {

            $sc =& $this->params['fields'][$fieldName];

            if ($sc['label'] === null) {
                $sc['label'] = 'Ent.' . \ucfirst($this->entityName) . '.Fields.' . $this->container->camelize($fieldName);
            }

            if ($sc['transformer'] === null) {
                $listTransformer = null;
                if (array_key_exists($fieldName, $meta->fieldMappings) ) {
                    $type = $meta->fieldMappings[$fieldName]['type'];

                    switch($type) {
                        case 'date':
                            $listTransformer = 'Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer';
                            break;
                    }
                }
                $sc['transformer'] = $listTransformer;
            }
        }
    }
    
    protected function deleteMessage($entity)
    {
        $transParams = array(
            '%name%' => $this->findEntityName($entity),
        );

        foreach($this->params()['on_remove']['cascade'] as $entName) {
            // @TODO check this, it call entity:getEditions on book, could be replaced with a query
            // with the entity parameters.
            // Also if the config name is different from the entity name, it will not work.
            // 
            $transParams['%nb_' . $entName . '%'] = $this->accessor->getValue($entity, $entName)->count();
        }

        foreach($this->params()['on_remove']['setnull'] as $entName) {
            // @TODO check this, it call entity:getEditions on book, could be replaced with a query
            // with the entity parameters.
            // Also if the config name is different from the entity name, it will not work.
            // 
            $transParams['%nb_' . $entName . '%'] = $this->accessor->getValue($entity, $entName)->count();
        }

        return $this->translator->trans($this->params()['l18n']['confirm_delete'], $transParams);
    }
}
